package cool.houge.pig.infra.service;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import cool.houge.pig.BizCodeException;
import cool.houge.pig.BizErrors;
import cool.houge.pig.PigProps;
import cool.houge.pig.StacklessBizCodeException;
import cool.houge.pig.UserStatus;
import cool.houge.pig.model.ActivationMail;
import cool.houge.pig.model.User;
import cool.houge.pig.repository.register.ActivationMailRepository;
import cool.houge.pig.repository.user.UserRepository;
import cool.houge.pig.service.register.EmailActivateInput;
import cool.houge.pig.service.register.EmailActivateOutput;
import cool.houge.pig.service.register.EmailRegisterInput;
import cool.houge.pig.service.register.EmailRegisterOutput;
import cool.houge.pig.service.register.RegisterService;
import cool.houge.pig.service.webhook.SharedWebhookService;
import cool.houge.pig.service.webhook.WebhookInput;
import cool.houge.pig.service.webhook.WebhookType;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;
import org.simplejavamail.email.EmailBuilder;
import org.simplejavamail.mailer.MailerBuilder;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;

/** @author KK (kzou227@qq.com) */
@Service
public class RegisterServiceImpl implements RegisterService {

  private final PigProps pigProps;
  private final UserRepository userRepository;
  private final ActivationMailRepository activationMailRepository;

  private final SharedWebhookService webhookService;
  private final TextEncryptor textEncryptor;

  /**
   * @param pigProps
   * @param userRepository
   * @param activationMailRepository
   * @param webhookService
   */
  public RegisterServiceImpl(
      PigProps pigProps,
      UserRepository userRepository,
      ActivationMailRepository activationMailRepository,
      SharedWebhookService webhookService) {
    this.pigProps = pigProps;
    this.userRepository = userRepository;
    this.activationMailRepository = activationMailRepository;

    // 加密
    this.textEncryptor =
        Encryptors.text(
            pigProps.getRegister().getMailActivation().getEncryptPassword(),
            pigProps.getRegister().getMailActivation().getEncryptSlat());
    this.webhookService = webhookService;
  }

  @Transactional
  @Override
  public Mono<EmailRegisterOutput> register(EmailRegisterInput input) {
    var mailActivationEnabled = pigProps.getRegister().getMailActivation().isEnabled();
    var initStatus =
        mailActivationEnabled ? UserStatus.TO_BE_VERIFIED_EMAIL.code : UserStatus.NORMAL.code;
    var model =
        new User()
            .setEmail(input.getEmail())
            .setPassword(input.getPassword())
            .setStatus(initStatus);
    return userRepository
        .save(model)
        .delayUntil(
            user -> {
              if (!mailActivationEnabled) {
                // 执行 Webhook
                return invokeRegisterWebhook(user);
              }
              return sendActivationMail0(user);
            })
        .map(m -> new EmailRegisterOutput().setUid(m.getId()));
  }

  @Transactional
  @Override
  public Mono<EmailActivateOutput> activate(EmailActivateInput input) {
    String text;
    try {
      text = textEncryptor.decrypt(input.getCipher());
    } catch (RuntimeException e) {
      throw new StacklessBizCodeException(BizErrors.C810, e)
          .putContextValue("cipher", input.getCipher());
    }

    long uid;
    String captcha;
    try {
      String[] arr = text.split(",");
      uid = Long.parseLong(arr[0]);
      captcha = arr[1];
    } catch (RuntimeException e) {
      throw new StacklessBizCodeException(BizErrors.C810, e);
    }

    return userRepository
        .findById(uid)
        .flatMap(
            user ->
                activateMail0(user, captcha)
                    // 执行 Webhook
                    .then(invokeRegisterWebhook(user))
                    .thenReturn(new EmailActivateOutput().setEmail(user.getEmail()))
            //
            );
  }

  @VisibleForTesting
  Mono<Void> activateMail0(User user, String captcha) {
    return activationMailRepository
        .findByUidAndCaptcha(user.getId(), captcha)
        .switchIfEmpty(Mono.error(() -> new BizCodeException(BizErrors.C2027, "邮件激活码不存在")))
        .doOnNext(
            activationMail -> {
              // 数据版本不等于1的激活码数据统一认定为已经使用过
              if (!Objects.equals(activationMail.getVer(), 1)) {
                throw new BizCodeException(BizErrors.C2027, "邮件激活码已经使用");
              }
            })
        .flatMap(
            activationMail -> {
              // 将激活码更新为已使用状态
              return Mono.when(
                  activationMailRepository.updateVer(
                      activationMail.getId(), activationMail.getVer()),
                  userRepository.updateStatus(user.getId(), user.getVer(), UserStatus.NORMAL.code));
            });
  }

  @VisibleForTesting
  Mono<Void> sendActivationMail0(User user) {
    // 生成激活码
    String captcha = UUID.randomUUID().toString();
    String param = textEncryptor.encrypt(Strings.lenientFormat("%s,%s", user.getId(), captcha));
    String activationUrl =
        Strings.lenientFormat(
            "%s?cipher=%s", pigProps.getRegister().getMailActivation().getUrl(), param);

    // 邮件内容
    String htmlText;
    try {
      var bytes = ByteStreams.toByteArray(getClass().getResourceAsStream("/activation-email.html"));
      htmlText = new String(bytes, StandardCharsets.UTF_8);
    } catch (IOException e) {
      throw new IllegalArgumentException("加载 activation-email.html 文件错误", e);
    }
    htmlText =
        htmlText.replaceAll("\\{\\{title}}", pigProps.getRegister().getMailActivation().getTitle());
    htmlText = htmlText.replaceAll("\\{\\{activateUrl}}", activationUrl);

    var smtp = pigProps.getRegister().getMailActivation().getSmtpServer();
    var mailer =
        MailerBuilder.withSMTPServerHost(smtp.getHost())
            .withSMTPServerPort(smtp.getPort())
            .withSMTPServerUsername(smtp.getUsername())
            .withSMTPServerPassword(smtp.getPassword())
            .buildMailer();
    var email =
        EmailBuilder.startingBlank()
            .from(smtp.getUsername())
            .to(user.getEmail())
            .withSubject(pigProps.getRegister().getMailActivation().getTitle())
            .withHTMLText(htmlText)
            .buildEmail();

    // 发送邮件
    var sendMailMono =
        Mono.<Void>create(
            sink -> {
              var response = mailer.sendMail(email, true);
              response.onSuccess(sink::success);
              response.onException(sink::error);
            });
    // 保存邮件
    return activationMailRepository
        .save(
            new ActivationMail()
                .setUid(user.getId())
                .setCaptcha(captcha)
                .setExpireTime(
                    LocalDateTime.now()
                        .plus(pigProps.getRegister().getMailActivation().getExpireDuration())))
        .then(sendMailMono);
  }

  /**
   * @param user
   * @return
   */
  @VisibleForTesting
  Mono<Void> invokeRegisterWebhook(User user) {
    var metadata = WebhookInput.Metadata.builder().build();
    var body = WebhookInput.Body.builder().uid(user.getId()).build();
    var input =
        WebhookInput.builder().type(WebhookType.REGISTER).metadata(metadata).body(body).build();
    return webhookService.invoke(input);
  }
}
